Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
@proscom/prostore
Advanced tools
Prostore - это библиотека для работы с данными во фронтенде, вдохновленная apollo-client. Apollo Client решает только задачи работы с API-запросами GraphQL, в то время как Prostore позиционируется как общее расширяемое решение, позволяющее также работать с чисто фронтендовыми данными. В этом плане Prostore чем-то похож на mobx.
Prostore родился из необходимости быстрого решения частых задач в SPA проектах, написанных на React с использованием React Hooks, поэтому на данный момент код библиотеки ориентирован именно на работу с React.
Эти задачи включают в себя:
Создание цепочек зависимостей по данным
Запрос данных с бекенда с автоматическим обновлением компонента при их получении и обработкой индикатора загрузки и ошибок
Отправка данных на бекенд в виде императивных мутирующих операций, с отслеживанием состояния запроса
Синхронизация данных с localStorage
yarn add @proscom/prostore rxjs
//
npm install --save @proscom/prostore rxjs
Данные в Prostore хранятся в сторах. Каждый стор представляет собой некое хранилище, выполняющее какую-то одну функцию. Например, стором может быть состояние выполнения API-запроса, или данные о текущем пользователе.
В основе Prostore лежит RxJS - мощная библиотека для работы с Observable. Observable это как EventEmitter, только с одним типом событий - обновление данных. Но благодаря функциям-операторам из rxjs можно создавать цепочки зависимостей одних Observable от других. Поближе познакомиться с rxjs можно здесь.
Каждый стор в Prostore реализует следующий интерфейс:
export interface IStore<State> {
readonly state: State;
readonly state$: Observable<State>;
}
Это позволяет создавать из сторов цепочки зависимостей. Например, при обновлении данных о текущем пользователе можно автоматически перевыполнить API-запрос, зависящий от них. Также в любой момент можно получить актуальное состояние стора.
Для удобства есть базовый класс BehaviorStore
у которого state$
представляет собой BehaviorSubject
из rxjs.
Расширив этот класс, можно создавать свои собственные сторы, у которых
работа с состоянием похожа на классовые компоненты в React. Например,
import { BehaviorStore } from '@proscom/prostore';
class UserStore extends BehaviorStore {
constructor() {
// в конструкторе передаем начальное состояние
super({
user: null
});
}
updateUser(newUser) {
this.setState({ user: newUser });
}
}
При расширении BehaviorStore в конструктор базового класса надо передать первоначальное состояние стора - любой JS объект.
Это должен быть именно объект. Состояние не может быть массивом или простым типом. Поэтому если надо использова не-объект, то оберните его в объект, присвоив какому-нибудь ключу:
super({
data: [1, 2, 3]
});
В любом месте этого класса (а также снаружи, но это не рекомендуется)
можно вызывать функцию this.setState
, которая принимает обновление
состояния, либо функцию обновления состояния (как в реакте).
При вызове this.setState
происходит одноуровневое слияние старого
состояния с новым (типа newState = {...oldState, ...changes}
).
Если же передана функция, то она сразу вызывается и в аргумент ей передается текущее состояние, а вернуть она должна изменения.
Если нужно сбросить состояние целиком, например, чтобы удалить какие-то ключи,
можно воспользоваться более низкоуровневым вызовом this.state$.next(newState)
.
Создав такой стор, можно дальше подписаться на него стандартными средствами rxjs:
const userStore = new UserStore();
const subscription = userStore.state$.subscribe((state) => {
console.log('state changed', state);
});
userStore.updateUser('Tester');
subscription.unsubscribe();
// Выведет:
// state changed { user: null }
// state changed { user: 'Tester' }
https://codesandbox.io/s/prostore-example-behavior-jomsr
Для удобства работы с подписками в React, смотри библиотеку prostore-react
.
Иногда может быть полезно не применять изменения состояния стора сразу, а отложить их до следующего цикла. Так например делает React при работе с классовыми компонентами.
В Prostore есть класс AsyncBehaviorStore, который собирает все вызовы
this.setState
в текущем синхронном цикле и выполняет их все сразу
последовательно в следующем (с помощью setTimeout
).
Это может быть полезно, если стор может измениться более одного раза за синхронный цикл. С точки зрения подписчиков, AsyncBehaviorStore изменится только один раз, в то время как BehaviorStore вызовет своих подписчиков при каждом изменении.
import { AsyncBehaviorStore } from '@proscom/prostore';
class AsyncUserStore extends AsyncBehaviorStore {
constructor() {
super({
firstName: null,
lastName: null
});
}
setFirstName(firstName) {
this.setState({ firstName });
}
setLastName(lastName) {
this.setState({ lastName });
}
}
// ...
const userStore = new AsyncUserStore();
userStore.state$.subscribe((state) => {
console.log('state changed', state);
});
userStore.setFirstName('first');
userStore.setLastName('last');
// Выведет:
// state changed: { firstName: null, lastName: null }
// state changed: { firstName: 'first', lastName: 'last' }
https://codesandbox.io/s/prostore-example-async-kwnns
RequestStore это более высокоуровневая абстракция, которая представляет
собой состояние какого-либо запроса. Запрос - это произвольная функция,
возможно асинхронная, которая превращает свои параметры variables
в результат data
. Например, эта функция может выполнять GET HTTP-запрос
с помощью fetch, передавая variables
как query-параметры, и сохранять
тело результата как data
.
Состояние RequestStore имеет следующий тип:
export interface IRequestState<Vars, Data> {
data: Data | null;
loading: boolean;
loaded: boolean;
error: any;
variables: Vars | null;
}
У RequestStore есть основной метод, который можно вызывать снаружи:
async function loadData(
variables: Vars,
options: any = {}
): Promise<IRequestState<Vars, Data>> {}
При вызове этой функции запускается выполнение нового запроса данных с новыми variables. Функция завершается, когда запрос будет выполнен успешно либо с ошибкой.
Для создания собственного стора надо расширить класс RequestStore
,
переопределив функцию
export type IObservableData<Data> = Promise<Data> | Observable<Data>;
function performRequest(
variables: Vars,
options: Options
): IObservableData<Data>;
Пример можно посмотреть на CodeSandbox: https://codesandbox.io/s/prostore-example-request-h9641
В библиотеках prostore-apollo
и prostore-axios
доступны свои классы,
расширяющие RequestStore, реализующие GraphQL-запросы и обычные HTTP-запросы
соответственно.
При вызове конструктора RequestStore
необходимо передать аргумент типа:
export interface IRequestStoreParams<Vars, Data> {
// Первоначальное значение data
initialData?: Data;
// Функция, позволяющая пропустить вызов performRequest
// Эта функция должна вернуть undefined, если запрос не надо пропускать
// Любое другое возвращенное значение будет сохранено как data
skipQuery?: ISkipQueryFn<Vars, Data>;
// Функция, которая позволяет не просто перезаписать data,
// а объединить старую data с новой.
// Это может быть полезно для запросов с пагинацией
updateData?: IUpdateDataFn<Vars, Data>;
// Идентификатор этого стора для передачи данных с сервера
// при использовании Server-Side-Rendering
ssrId?: string;
}
export type ISkipQueryFn<Vars, Data> = (vars: Vars) => Data | null | undefined;
export type IUpdateDataFn<Vars, Data> = (
data: Data,
oldData: Data,
params: { store: any; variables: Vars; options: any }
) => Data;
Для удобства в качестве skipQuery
можно передать одну из двух
предопределенных функций:
import { skipIf, skipIfNull } from '@proscom/prostore';
new MyRequestStore({
// Если vars не удовлетворяет функции condition, то
// запрос не вызывается и data = defaultData
skipQuery: skipIf((vars) => !vars, defaultData),
// или
// Если vars равно null,
// то запрос не вызывается и data = defaultData
skipQuery: skipIfNull(defaultData)
});
SubscriptionManager хранит список подписок и позволяет отменить все активные подписки вызовом одной функции. Рекомендуется использовать его при взаимодействии сторов друг с другом. См. пример ниже в разделе Общение между сторами.
При работе с глобальными сторами рекомендуемый способ организации взаимодействия между двумя сторами - передать один из сторов в качестве аргумента в конструктор другого:
import { BehaviorStore, SubscriptionManager } from '@proscom/prostore';
interface IUserStoreArgs {
tokenStore: TokenStore;
}
interface IUserStoreState {
user: User;
}
class UserStore extends BehaviorStore<IUserStoreState> {
sub = new SubscriptionManager();
constructor({ tokenStore }: IUserStoreArgs) {
super({ user: null });
// Подписка на другой стор в конструкторе
this.sub.subscribe(tokenStore.state$, this.handleTokenStoreChange);
}
// Функция-деструктор на случай, если стор будет использован не глобально и потребуется сброс подписки
destroy() {
this.sub.destroy();
}
handleTokenStoreChange = (tokenState) => {
// Здесь можно использовать состояние другого стора, чтобы актуализировать состояние этого стора
this.setState({ user: tokenState.user });
};
}
const tokenStore = new TokenStore();
const userStore = new UserStore({ tokenStore });
Рекомендуется избегать циклической зависимости между сторами, но если она необходима, то можно передавать один стор в другой динамически:
import { BehaviorStore, SubscriptionManager } from '@proscom/prostore';
class UserStore extends BehaviorStore<IUserStoreState> {
sub = new SubscriptionManager();
constructor() {
super({ user: null });
}
setTokenStore(tokenStore: TokenStore) {
this.sub.subscribe(tokenStore.state$, this.handleTokenStoreChange);
}
// destroy и handleTokenStoreChange аналогичны примеру выше
}
const userStore = new UserStore();
const tokenStore = new TokenStore({ userStore });
userStore.setTokenStore(tokenStore);
Избежать циклических зависимостей также можно, если использовать шину событий для взаимодействия между сторами. Шина событий также поможет связать локальные сторы друг с другом, даже если у них нет прямых ссылок друг на друга.
Шина событий - это просто Observable, через который проходят какие-то события (можно использовать что-то наподобие Actions из Redux). Стор подписывается на события этого обзервабла и может каким-то образом реагировать на них.
import { Subject } from 'rxjs';
interface IStoreEvent {
type: string;
}
const EVENT_DATA_INCREMENT = 'EVENT_DATA_INCREMENT';
// Subject - это особый вид Observable, который не дублирует переданные значения
const storeEvents$ = new Subject<IStoreEvent>();
class DataStore extends BehaviorStore {
sub = new SubscriptionManager();
constructor() {
super({ value: 0 });
this.sub.subscribe(storeEvents$, this.handleStoreEvent);
}
destroy() {
this.sub.destroy();
}
handleStoreEvent = (event) => {
if (event.type === EVENT_DATA_INCREMENT) {
this.setState({ value: this.state.value + 1 });
}
};
}
const dataStore = new DataStore();
storeEvents$.next({ type: EVENT_DATA_INCREMENT });
Шину событий можно также использовать для императивного слабосвязанного взаимодействия между компонентами. Используйте ее аккуратно, чтобы не запутывать код.
При использовании prostore-react
шину событий можно подключить в компонент с помощью хука useObservable
:
function MyComponent() {
// Можно использовать глобальную переменную storeEvents$
// но лучше пробросить её через контекст (useContext)
useObservable(storeEvents$, (event) => {
// Сделать что-то с storeEvents$
});
}
В случаях когда нужна пагинация с дозагрузкой (например, бесконечный скролл с дозагрузкой)
можно использовать функцию updateData
.
Пример:
const myStore = new MyRequestStore({
updateData: (data, oldData, params) => {
const page = params.variables.page;
const perPage = params.variables.perPage;
return [
...oldData?.slice(0, page * perPage),
...data,
...oldData?.slice((page + 1) * perPage)
];
}
});
let state;
state = await myStore.loadData({ page: 0, perPage: 2 });
// data=[1,2] oldData=null params={ variables: { page: 0, perPage: 2 }, ... }
// state.data = [1,2]
state = await myStore.loadData({ page: 1, perPage: 2 });
// data=[3,4] oldData=[1,2] params={ variables: { page: 1, perPage: 2 }, ... }
// state.data = [1,2,3,4]
FAQs
State management library with multiple stores based on rxjs
The npm package @proscom/prostore receives a total of 11 weekly downloads. As such, @proscom/prostore popularity was classified as not popular.
We found that @proscom/prostore demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.